﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using XcoAppSpaces.Contracts.Security;
using Microsoft.Ccr.Core;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Service;
using XcoAppSpaces.Contracts.Logging;
using XcoAppSpaces.Contracts.Worker;

namespace XcoAppSpaces.Security.Basic
{
	//TODO:
	//- How to recognize that a session has ended? (How to remove old sessions?) -> Leases, Timeout
	//- Define permissions for worker extensions
	//- Add hash to users file?

	/// <summary>
	/// The XcoBasicSecurityService provides a simple security solution for the XcoAppSpace.
	/// Authentication is based on user/password, authorization on roles which can be assigned
	/// to users. The users can be stored to and loaded from an XML file.
	/// </summary>
	public class XcoBasicSecurityService : IXcoSecurityService
	{
		private IXcoLogger log = null;
		private readonly UserStore userStore = null;
		private bool loadUsersFromStore = false;
		private readonly Dictionary<string, User> users = new Dictionary<string, User>();
		private readonly Dictionary<IPort, WorkerPermissionInfo> workerPermissions = new Dictionary<IPort, WorkerPermissionInfo>();
		private readonly Dictionary<Guid, Session> sessions = new Dictionary<Guid, Session>();
		private XcoSecurityPolicy policy = XcoSecurityPolicy.Permissive;
		private IXcoWorkerRegistry workerRegistry = null;

		/// <summary>
		/// Creates a new XcoBasicSecurityService. Users are automatically loaded from the given file path,
		/// if the file exists. The SaveUsers() method will also automatically use this path to save the
		/// users file.
		/// </summary>
		/// <param name="userStorePath">The path of the users xml file.</param>
		/// <param name="policy">The security policy that should be used.</param>
        public XcoBasicSecurityService(string userStorePath, XcoSecurityPolicy policy)
        {
            this.policy = policy;
            userStore = new UserStore(userStorePath);
            loadUsersFromStore = true;
        }

		/// <summary>
		/// Creates a new XcoBasicSecurityService. Users are automatically loaded from the given file path,
		/// if the file exists. The SaveUsers() method will also automatically use this path to save the
		/// users file.
		/// </summary>
		/// <param name="userStorePath">The path of the users xml file.</param>
        public XcoBasicSecurityService(string userStorePath)
            : this(userStorePath, XcoSecurityPolicy.Permissive)
		{
		}

		/// <summary>
		/// Creates a new XcoBasicSecurityService.
		/// </summary>
		public XcoBasicSecurityService()
		{
			userStore = new UserStore(null);
			loadUsersFromStore = false;
		}

		/// <summary>
		/// The security policy that should be used. Is permissive by default.
		/// </summary>
		public XcoSecurityPolicy Policy
		{
			get { return policy; }
			set { policy = value; }
		}

		/// <summary>
		/// The file path where users should be loaded or stored. If this property
		/// is set before the XcoAppSpace is instantiated, the users will automatically
		/// be loaded from this path.
		/// </summary>
		public string StorePath
		{
			get { return userStore.StorePath; }
			set { userStore.StorePath = value; loadUsersFromStore = true; }
		}

		#region User Management
		/// <summary>
		/// Adds the user with the given name, passwort and roles to the security service.
		/// An XcoSecurityException is thrown if a user with the given name already exists.
		/// </summary>
		/// <param name="name">The name of the user.</param>
		/// <param name="password">The password of the user.</param>
		/// <param name="roles">The roles of the user.</param>
		public void AddUser(string name, string password, params string[] roles)
		{
			lock (users)
			{
				if (users.ContainsKey(name))
					throw new XcoSecurityException("A user with this name already exists.");
				users.Add(name, new User(name, PasswordUtil.CreatePasswordHash(password), roles));
			}
		}

		/// <summary>
		/// Removes the user with the given name from the security service.
		/// Existing sessions for this user are removed.
		/// </summary>
		/// <param name="name">The name of the user to be removed.</param>
		public void RemoveUser(string name)
		{
			lock (users)
			{
				if (users.ContainsKey(name))
					users.Remove(name);
				lock (sessions)
				{
					var userSessions = sessions.Values.Where(s => s.User == name).Select(s => s.SessionID).ToList();
					foreach (var s in userSessions)
						sessions.Remove(s);
				}
			}
		}

		/// <summary>
		/// Adds a role to the user with the given name.
		/// Note: Existing sessions are not updated with this information.
		/// </summary>
		/// <param name="username">The name of the user to which the role should be added.</param>
		/// <param name="role">The role that should be added.</param>
		public void AddRole(string username, string role)
		{
			lock (users)
			{
				if (!users.ContainsKey(username))
					throw new XcoSecurityException("A user with this name does not exist.");
				if (!users[username].Roles.Contains(role))
					users[username].Roles.Add(role);
			}
		}

		/// <summary>
		/// Removes a role from the user with the given name.
		/// Note: Existing sessions are not updated with this information.
		/// </summary>
		/// <param name="username">The name of the user from which the role should be removed.</param>
		/// <param name="role">The role that should be removed.</param>
		public void RemoveRole(string username, string role)
		{
			lock (users)
			{
				if (!users.ContainsKey(username))
					throw new XcoSecurityException("A user with this name does not exist.");
				if (users[username].Roles.Contains(role))
					users[username].Roles.Remove(role);
			}
		}

		/// <summary>
		/// Saves the users to a file. If a file path has been specified, the file
		/// is saved to this path. Otherwise, a standard path is used, which is the
		/// path of the application, and the file name "XcoAppSpaces.Security.Basic_Users.xml".
		/// </summary>
		public void SaveUsers()
		{
			List<User> userList = new List<User>();
			foreach (User user in users.Values)
			{
				userList.Add(user);
			}
			userStore.Save(userList);
			if (log != null)
				log.Info("Users saved.");
		}
		#endregion

		#region IXcoSecurityService Members

		/// <summary>
		/// Authenticates access for the given user to the space. It is checked if the username and password
		/// are valid. A sessionID is generated if authentication is successful. If the user already has an open
		/// session, the id of the existing session is returned.
		/// </summary>
		/// <param name="address">The host address of the user.</param>
		/// <param name="credentials">The credentials of the user.</param>
		/// <returns>The result of the authentication, including the Guid of the user session if it was successful.</returns>
		public XcoAuthenticationResult Authenticate(string address, IXcoClientCredentials credentials)
		{
			log.Debug("User " + (credentials != null ? credentials.User : "[anonymous]") + " authenticating.");
			
			if (credentials != null)
			{
				if (!users.ContainsKey(credentials.User) || !PasswordUtil.AreEqual(users[credentials.User].Password, credentials.Password))
				{
					//unknown user or wrong password
					log.Warn("User " + credentials.User + " tried to connect -> not authenticated!");
					return new XcoAuthenticationResult { IsAuthenticated = false, SessionID = Guid.Empty };
				}
			}
			else
			{
				//no credentials given -> anonymous user
				if (policy == XcoSecurityPolicy.Restrictive)
				{
					//if security service is in restrictive mode, anonymous users are not allowed
					log.Warn("Anonymous user tried to connect -> not allowed in restrictive mode!");
					return new XcoAuthenticationResult { IsAuthenticated = false, SessionID = Guid.Empty };
				}
			}
			
			//check if session already exists and return session id
			string user = (credentials != null ? credentials.User : null);
			Session session = sessions.Values.FirstOrDefault(s => s.HostAddress == address && s.User == user);
			if (session == null)
			{
				session = new Session(Guid.NewGuid(), (credentials != null ? credentials.User : null), address);
				sessions.Add(session.SessionID, session);
			}
			return new XcoAuthenticationResult { IsAuthenticated = true, SessionID = session.SessionID };
		}

		/// <summary>
		/// Authorizes access for the user of the given session to the given worker. Authorization is successful
		/// if the user is allowed to access at least one port of the worker.
		/// </summary>
		/// <param name="address">The host address of the user.</param>
		/// <param name="sessionID">The sessionID of the user (that was generated during user authentication).</param>
		/// <param name="worker">The worker instance.</param>
		/// <returns>True if the user is authorized to access the worker.</returns>
		public bool Authorize(string address, Guid sessionID, IPort worker)
		{
			//check session
			if (!sessions.ContainsKey(sessionID))
			{
				log.Warn("Permission check for session " + sessionID + ": SessionID is unknown!");
				return false;
			}
			Session session = sessions[sessionID];
			if (!session.HostAddress.Equals(address, StringComparison.OrdinalIgnoreCase))
			{
				log.Warn("Permission check for session " + sessionID + ": Host address " + address + " is not authorized to work with this session!");
				return false;
			}
			log.Debug("User " + (session.User ?? "[anonymous]") + " authorizing for worker " + worker);

			if (session.Workers.Exists(w => w.Worker == worker))
			{
				//no need to check allowed message types again if worker is already part of this session
				return true;
			}
			
			//check worker
			if (!workerPermissions.ContainsKey(worker))
			{
				log.Warn("XcoBasicSecurityService: Worker " + worker + " is unknown!");
				return false;
			}
			WorkerPermissionInfo workerInfo = workerPermissions[worker];

			List<Type> allowedMessageTypes = workerInfo.GetAllowedMessageTypes(session.IsAnonymous ? null : users[session.User]);
			if (allowedMessageTypes != null && allowedMessageTypes.Count > 0)
			{
				session.Workers.Add(new WorkerSessionInfo(worker, allowedMessageTypes));
				return true;
			}
			return false;
		}

		/// <summary>
		/// Checks if certain port of a worker can be accessed by a specific session. An XcoSecurityException is thrown
		/// if the operation is not permitted.
		/// </summary>
		/// <param name="worker">The worker that should be accessed.</param>
		/// <param name="message">The message that should be posted to the worker.</param>
		/// <param name="address">The address ot the sender of the message.</param>
		/// <param name="sessionID">The ID of the session that the sender uses for communication.</param>
		public void CheckPermission(IPort worker, object message, string address, Guid sessionID)
		{
			log.Debug("Checking permission of worker " + worker.ToString() + " for session " + sessionID);
			if (!sessions.ContainsKey(sessionID))
			{
				log.Warn("Permission check for session " + sessionID + ": SessionID is unknown!");
				throw new XcoSecurityException("SessionID is unknown!");
			}
			if (sessions[sessionID].HostAddress != address)
			{
				log.Warn("Permission check for session " + sessionID + ": Host address " + address + " is not authorized to work with this session!");
				throw new XcoSecurityException("Host address " + address + " not authorized to work with this session!");
			}
			WorkerSessionInfo workerInfo = sessions[sessionID].Workers.FirstOrDefault(w => w.Worker == worker);
			if (workerInfo == null)
			{
				log.Warn("Permission check for session " + sessionID + ": SessionID is not authorized to access given worker " +
				         worker + "!");
				throw new XcoSecurityException("SessionID is not authorized to access worker!");
			}
			if (!workerInfo.IsAllowed(message.GetType()))
			{
				log.Warn("Permission check for session " + sessionID + ": The given message type " + message.GetType() +
				         " is not allowed for this user!");
				throw new XcoSecurityException("The given message type " + message.GetType() + " is not allowed for this user!");
			}
		}

		/// <summary>
		/// Gets xco client credentials from a given credentials object and for the given address, that can be 
		/// transfered to a remote space and used to authenticate a user at a remote space.
		/// </summary>
		/// <param name="credentials">The ICredentials object where to read the credentials from.</param>
		/// <param name="address">The address for which the credentials are needed.</param>
		/// <returns>A new IXcoClientCredentials instance containing the needed authentication information.</returns>
		public IXcoClientCredentials GetClientCredentials(ICredentials credentials, string address)
		{
			return credentials.GetXcoClientCredentials(address);
		}

		/// <summary>
		/// Gets the username for the given session id.
		/// </summary>
		/// <param name="sessionID">The id of the session for which the user should be retrieved.</param>
		/// <returns>The username for the given session, or null if no user is applied to this session.</returns>
		public string GetUsername(Guid sessionID)
		{
			if (sessions.ContainsKey(sessionID))
				return sessions[sessionID].User;
			return null;
			//throw new XcoSecurityException("Error getting username for session with ID " + sessionID + ": Session is unknown!");
		}

		#endregion

		#region IXcoStatefulService Members

		/// <summary>
		/// Starts the service. Loads the list of users from a file if requested,
		/// and registers itself to the local worker registry to get events
		/// when workers are added or removed.
		/// </summary>
		public void Start()
		{
			if (loadUsersFromStore)
			{
				List<User> userList = userStore.Load();
				if (userList != null)
				{
					foreach (User user in userList)
					{
						users.Add(user.Name, user);
					}
					log.Debug("XcoBasicSecurityService: " + userList.Count + " Users loaded from store.");
				}
			}
			workerRegistry.WorkerAdded += workerRegistry_WorkerAdded;
			workerRegistry.WorkerRemoved += workerRegistry_WorkerRemoved;
			log.Debug("XcoBasicSecurityService started.");
		}

		private void workerRegistry_WorkerAdded(object sender, XcoWorkerInfo workerInfo)
		{
			if (workerInfo.IsPublished)
			{
				log.Debug("Worker added to security service: " + workerInfo.Worker.ToString());
				workerPermissions.Add(workerInfo.Worker, new WorkerPermissionInfo(workerInfo.Worker, workerInfo.ServiceAttributes, log));
			}
		}

		private void workerRegistry_WorkerRemoved(object sender, XcoWorkerInfo workerInfo)
		{
			if (workerInfo.IsPublished)
			{
				log.Debug("Worker removed from security service: " + workerInfo.Worker.ToString());
				workerPermissions.Remove(workerInfo.Worker);
			}
		}

		/// <summary>
		/// Start priority of the service.
		/// </summary>
		public int StartPriority
		{
			get { return 3; } //after connector, before transport services
		}

		/// <summary>
		/// Stops the service.
		/// </summary>
		public void Stop()
		{
			workerRegistry.WorkerAdded -= workerRegistry_WorkerAdded;
			workerRegistry.WorkerRemoved -= workerRegistry_WorkerRemoved;
		}

		#endregion

		#region IXcoService Members

		/// <summary>
		/// Initializes the service. Gets the instance of the local worker registry
		/// which is needed by the security service for recognizing when workers
		/// are added or removed.
		/// </summary>
		/// <param name="serviceRegistry"></param>
		public void Initialize(IXcoServiceRegistry serviceRegistry)
		{
			this.log = XcoLoggerFactory.CreateInstance(typeof(XcoBasicSecurityService));
			this.workerRegistry = serviceRegistry.Resolve<IXcoWorkerRegistry>();
		}

		#endregion
	}
}
